//---------------------------------------------------------------------
// File: DotNetObjectInvokerExStep.cs
// 
// Summary: 
//
// Copyright (c) Hammersmith & Fulham Bridge Partnership. All rights reserved.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
// KIND, WHETHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
// PURPOSE.
//---------------------------------------------------------------------

using System;
using System.Collections;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.Services.BizTalkApplicationFramework.BizUnit;

namespace BizUnit.Extensions
{
	/// <summary>
	/// The DotNetObjectInvokerStep is a sort of extension mechansim. It is used to call out to any .NET component that can accept
	/// requests and return responses. This allows developers to invoke their components from BizUnit and do stuff that is not 
	/// possible in BizUnit itself. The EXTENSION now provides the facility to use the context while setting the parameter values
	/// </summary>
	/// 
	/// <remarks>
	/// The following shows an example of the Xml representation of this test step.
	/// In this example we are invoking a method called TestEndRequest3 in a component named ServiceLevelTracking
	/// and giving the invoker the fully qualified type name. The method TestEndRequest3 takes 2 parameters, a string and an integer
	/// and we explicitly indicate this to the invoker.
	/// 
	/// <code escaped="true">
	///	<TestStep assemblyPath="" typeName="BizUnit.Extensions.DotNetObjectInvokerStep">
	///		<TypeName>Microsoft.Samples.BizTalk.WoodgroveBank.ServiceLevelTracking.ServiceLevelTracking, Microsoft.Samples.BizTalk.WoodgroveBank.ServiceLevelTracking, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a1054514fc67bded</TypeName>	
	///		<AssemblyPath></AssemblyPath>
	///		<MethodToInvoke>TestEndRequest3</MethodToInvoke>
	///		<Parameter><string>fooBar</string></Parameter>
	///		<Parameter><int>123</int></Parameter>
	///		<ReturnParameter><int>barfoo</int></ReturnParameter>
	///	</TestStep>
	///	</code>
	///	
	///	<list type="table">
	///		<listheader>
	///			<term>Tag</term>
	///			<description>Description</description>
	///		</listheader>
	///		<item>
	///			<term>TypeName</term>
	///			<description>The name of the type of the .Net object to invoke the method on. Note, if the type is GAC'd then the assembly name, version, public key etc need to be specified</description>
	///		</item>
	///		<item>
	///			<term>AssemblyPath</term>
	///			<description>The path to the assembly <para>(optional)</para></description>
	///		</item>
	///		<item>
	///			<term>MethodToInvoke</term>
	///			<description>The name of the method to invoke</description>
	///		</item>
	///		<item>
	///			<term>Parameter</term>
	///			<description>The value for the parameter to pass into the method. Note: the format should be the serialised .Net type <para>(optional | multiple)</para></description>
	///		</item>
	///		<item>
	///			<term>ReturnParameter</term>
	///			<description>The value returned from the method <para>(optional)</para></description>
	///		</item>
	///	</list>
	///	</remarks>

	public class DotNetObjectInvokerExStep : ITestStep
	{
		/// <summary>
		/// ITestStep.Execute() implementation
		/// </summary>
		/// <param name='testConfig'>The Xml fragment containing the configuration for this test step</param>
		/// <param name='context'>The context for the test, this holds state that is passed beteen tests</param>
		public void Execute(XmlNode testConfig, Context context)
		{
			// read test config...
			string typeName = context.ReadConfigAsString( testConfig, "TypeName" );
			string assemblyPath = context.ReadConfigAsString( testConfig, "AssemblyPath" );
			string methodToInvoke = context.ReadConfigAsString( testConfig, "MethodToInvoke" );
			string returnParameter = context.ReadConfigAsXml( testConfig, "ReturnParameter" );
			XmlNodeList parameters  = testConfig.SelectNodes("Parameter");
			object obj = CreateObject(typeName, assemblyPath, context);


			//Replace parameter values with values taken from context
			string newParameterValue = string.Empty;
			string key= string.Empty;

			foreach(XmlNode node in parameters)
			{
				if(node.SelectSingleNode("@takeFromCtx")!=null)
				{
					key = node.SelectSingleNode("@takeFromCtx").Value;
					try
					{
						newParameterValue = context.GetValue(key);
						//rewrite the node value
						if(newParameterValue.Length>0) 
                        {
						// WS Edit: Set node.InnerText rather than node.InnerXML, so that parameter can contain XML tags if necessary.
							node.InnerText = newParameterValue;
						}
					}
					catch (NullReferenceException NREx)
					{
						context.LogError(String.Format("Could not find a value for the context key {0}", key));
						throw NREx;
					}
					catch (XmlException XmlEx)
					{
						context.LogError(String.Format("There was a problem when reading context key {0} into the XML Node {1}"), key, node.LocalName);
						throw XmlEx;
					}
					catch (Exception)
					{
						context.LogError("There was a problem while reading data from the context loader");
						throw;
					}
				}
			}

			
			MethodInfo mi = obj.GetType().GetMethod(methodToInvoke);
			ParameterInfo[] pi = mi.GetParameters();
			
			ArrayList parameterArray = new ArrayList();
			try
			{
				for( int c = 0; c < pi.Length; c++)
				{
					Type t = pi[c].ParameterType;
					XmlSerializer xs = new XmlSerializer(t);
					// WS/Benjy added test for < and > 
					// WS removed deserializer. Read parameters[c] as InnerText rather than InnerXML, so that strings can contain XML if needed.
					parameterArray.Add(Convert.ChangeType(parameters[c].InnerText, t));
					//
					//				parameterArray.Add( xs.Deserialize(new XmlTextReader(StreamHelper.LoadMemoryStream(parameters[c].InnerXml))) );
				}
			}
			catch(Exception e)
			{
				
				string exMsg = "There was an error when attempting to deserialize your parameters. Please check they can be converted to the correct type for the method you are trying to call." + e.Message;
				context.LogException(new Exception(exMsg, e));
			}


			object[] paramsForCall = new object[parameterArray.Count];
			for( int c = 0; c < parameterArray.Count; c++ )
			{
				paramsForCall[c] = parameterArray[c];
			}

			context.LogInfo("About to call the method: {0}() on the type {1}", methodToInvoke, typeName );
			// Call the .Net Object...
			object returnValue = mi.Invoke(obj, paramsForCall);
			context.LogInfo("Return value: {0}", returnValue);

			if ( returnParameter.Length > 0 )
			{
				XmlSerializer xsRet = new XmlSerializer(returnValue.GetType());

				MemoryStream rs = new MemoryStream();
				xsRet.Serialize( new StreamWriter(rs), returnValue );
				MemoryStream es = StreamHelper.LoadMemoryStream(returnParameter);

				rs.Seek(0, SeekOrigin.Begin);
				es.Seek(0, SeekOrigin.Begin);
				StreamHelper.CompareXmlDocs(rs, es, context);
			}
		}

		static private object CreateObject(string typeName, string assemblyPath, Context context)
		{
			object comp = null;
			Type ty = null;

			context.LogInfo("About to create the folowing .Net type: {0}", typeName);

			if (assemblyPath!=null && assemblyPath.Length != 0) 
			{
				context.LogInfo("Loading assembly form path: {0}", assemblyPath);

				Assembly assembly = Assembly.LoadFrom(assemblyPath);
				ty = assembly.GetType(typeName, true, false);
			}
			else 
			{
				ty = Type.GetType(typeName);
			}

			if (ty != null) 
			{
				comp = Activator.CreateInstance(ty);
			}
			
			return comp;
		}
	}
}
